using System;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;
#if UNITY_EDITOR
using UnityEditor;
#endif

public class PolyTerrain : MonoBehaviour
{
    private int _seed;

    private int[,] _verticesHeight;
    public int GridSize = 10;
    public float HeightScale = 0.5f;
    public PolyTerrainModuleDescriptor[] Modules;
    public int Period = 5;

    public int Seed
    {
        get => _seed;
        set
        {
            _seed = value;
            GenerateTerrain();
        }
    }

    // Start is called before the first frame update
    private void Start()
    {
        GenerateTerrain();
    }

    public void GenerateTerrain()
    {
        _verticesHeight = new int[GridSize, GridSize];

        CleanupModules();
        InitializeTerrainWithNoise();
        ClampTerrainGradient(out var averageHeight);
        MoveTerrainToCenter(averageHeight);
        AssembleModuleMesh();
    }

    private void CleanupModules()
    {
        var childCount = transform.childCount;

        for (var i = childCount - 1; i >= 0; i--)
        {
            var child = transform.GetChild(i);
            if (Application.isPlaying)
                Destroy(child.gameObject);
            else
                DestroyImmediate(child.gameObject);
        }
    }

    /// <summary>
    ///     Terrain may not locate at average z = 0, so could be immensed by water
    ///     Let's pull up it
    /// </summary>
    /// <param name="averageHeight"> Average height of this terrain vertices </param>
    private void MoveTerrainToCenter(int averageHeight)
    {
        for (var i = 0; i < GridSize; i++)
        for (var j = 0; j < GridSize; j++)
            _verticesHeight[i, j] -= averageHeight;
    }

    /// <summary>
    ///     We have a constraint: neighbor vertex cannot height difference cannot be larger than 1
    ///     Usually, a gradient based noise with small step should always give good result,
    ///     However, if the noise is too steep, we have to clamp the gradient
    /// </summary>
    /// <param name="averageHeight"> Average height of this terrain vertices </param>
    private void ClampTerrainGradient(out int averageHeight)
    {
        averageHeight = 0;

        for (var i = 0; i < GridSize; i++)
        for (var j = 0; j < GridSize; j++)
        {
            var valueOfVertex = _verticesHeight[i, j];
            var valueLeft = i <= 0 ? valueOfVertex : _verticesHeight[i - 1, j];
            var valueBottom = j <= 0 ? valueOfVertex : _verticesHeight[i, j - 1];

            if (Math.Abs(valueLeft - valueBottom) > 2)
            {
                if (i == 0)
                {
                    var offset = Math.Clamp(valueLeft - valueBottom, -1, 1);
                    _verticesHeight[i, j] = valueBottom + offset;
                }
                else
                {
                    var offset = Math.Clamp(valueBottom - valueLeft, -1, 1);
                    _verticesHeight[i, j] = valueLeft + offset;
                }
            }
            else if (Math.Abs(valueOfVertex - valueLeft) > 1 || Math.Abs(valueOfVertex - valueBottom) > 1)
            {
                _verticesHeight[i, j] = (valueLeft + valueBottom) / 2;
            }

            if ((Math.Abs(_verticesHeight[i, j] - valueLeft) > 1 && i > 0) ||
                (Math.Abs(_verticesHeight[i, j] - valueBottom) > 1 && j > 0))
                Debug.LogError(string.Format("Too steep gradient at {0}, {1}, with left {2}, bottom {3}, vertex {4}", i,
                    j, valueLeft, valueBottom, _verticesHeight[i, j]));

            averageHeight += _verticesHeight[i, j];
        }

        averageHeight /= GridSize * GridSize;
    }


    /// <summary>
    ///     Guess with a gradient based noise
    ///     Gradient based noise has good property of limited gradient, so we start with this
    /// </summary>
    private void InitializeTerrainWithNoise()
    {
        Random.InitState(_seed);
        var randomOffset = Random.Range(0.0f, 1.0f);

        /* Guess with a gradient based noise */
        for (var i = 0; i < GridSize; i++)
        for (var j = 0; j < GridSize; j++)
        {
            var noiseValue =
                noise.pnoise(new float2((float)i / GridSize + randomOffset, (float)j / GridSize + randomOffset),
                    new float2(Period, Period));
            _verticesHeight[i, j] = (int)(noiseValue * GridSize * HeightScale);
        }
    }

    private void AssembleModuleMesh()
    {
        for (var i = 0; i < GridSize - 1; i++)
        for (var j = 0; j < GridSize - 1; j++)
        {
            var cornerHeights = new int[4]
            {
                _verticesHeight[i, j], _verticesHeight[i + 1, j], _verticesHeight[i + 1, j + 1],
                _verticesHeight[i, j + 1]
            };

            var minHeight = int.MaxValue;
            foreach (var height in cornerHeights)
                if (height < minHeight)
                    minHeight = height;
            for (var k = 0; k < 4; k++) cornerHeights[k] -= minHeight;

            var moduleCenterPosition = new Vector3(i + 0.5f, minHeight * 0.5f, j + 0.5f);

            foreach (var module in Modules)
                if (module.Fit(cornerHeights, out var rotationDegree))
                {
                    var moduleInstance = Instantiate(module.gameObject, moduleCenterPosition,
                        Quaternion.Euler(0, rotationDegree, 0), transform);
                    moduleInstance.name = string.Format("module{0}-{1}", i, j);
                }
        }
    }

    private void OnGUI()
    {
        HeightScale = GUI.HorizontalSlider(new Rect(10, 80, 200, 40), HeightScale, 0, 2);

        // Display the current slider value
        GUI.Label(new Rect(220, 80, 200, 40), String.Format("Height Scale: {0}", HeightScale));
        if (GUI.Button(new Rect(10, 10, 100, 50), "Randomize"))
        {
            Seed = (int)Random.Range(0, 10000);
        }
    }

#if UNITY_EDITOR
    public void OnDrawGizmos()
    {
        if (_verticesHeight == null) return;

        for (var i = 0; i < GridSize; i++)
        for (var j = 0; j < GridSize; j++)
        {
            var position = new Vector3(i, _verticesHeight[i, j] * 0.5f, j);
            Gizmos.color = Color.yellow;
            Gizmos.DrawSphere(position, 0.01f);
            Handles.color = Color.red;
            Handles.Label(position, string.Format("{0}-{1}, {2}", i, j, _verticesHeight[i, j]));
        }
    }
#endif
}